查看原文
其他

Android程序设计探索:MVP与模块化

2017-07-05 奔ben苯笨 终端研发部

前言介绍

MVP与模块化,关于Android程序设计探索。

该篇是来自于奔ben苯笨的投稿,一个很有思想的老司机。

奔ben苯笨的博客地址:

http://www.jianshu.com/p/fb057953131e

MVP介绍

0. 背景

最早接触到MVP这种设计模式,是在14年读《打造高质量Android应用:Android开发必知的50个诀窍》一书中了解到,而之后也逐步尝试去使用,至今体验下来,它不是一个可以完美到可以生搬硬套到各个场景的模式,正确地使用才能最好地发挥它的作用。

1. 作用简介

  • 分层:将代码分层,抽取出数据、模型、界面。

  • 复用:对V层或者P层接口的多种实现。

2. 作用-分层

我们大部分对MVP着迷的一个原因是早期写业务复杂的Activity时,代码量过于庞大,导致可读性很差。
而MVP通过3层的分离,有效地减少了Activity的代码量。
对于这个作用的理解上,个人认为,只有代码量比较大(大于1000行),并且Activity内各个功能模块比较耦合的时候,适用MVP模式。

3. 作用-复用

这是MVP的另一个非常优雅的使用场景。

  • 当需要实现多个布局界面,但业务逻辑却不相同的场景时(即一个V层对应多个P层),MVP非常适用。

  • 当然,多个布局架构不一致,但业务逻辑一致的情况(即一个P层对应多个V层),MVP也适用,不过至今我还遇到这种情况。

以下举个案例:
需求是实现多个以下的界面,布局架构一致,但数据内容、触发逻辑都不相同。





①. V层

package com.benhero.design.mvp.view;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.benhero.design.R;
import com.benhero.design.mvp.bean.MvpItem;
import com.benhero.design.mvp.presenter.MvpPresenterD;
import com.benhero.design.mvp.presenter.MvpContract;
import com.benhero.design.mvp.presenter.MvpPresenterB;
import com.benhero.design.mvp.presenter.MvpPresenterC;
import com.benhero.design.mvp.presenter.MvpPresenterA;
import java.util.ArrayList;
import java.util.List;

/** * MVP * * @author benhero */
public class MvpActivity extends AppCompatActivity implements MvpContract.View, View.OnClickListener {    
   public static final String EXTRA_ENTER = "enter";    
   /**     * 1 : A     */    public static final int ENTER_A = 1;    
  /**     * 2 : B     */    public static final int ENTER_B = 2;    
  /**     * 3 : C     */    public static final int ENTER_C = 3;    
   /**     * 4 : D     */    public static final int ENTER_D = 4;    
   private MvpContract.Presenter mPresenter;    
   private TextView mUpgradeBtn;    
   private ListView mListView;    
   private List<MvpItem> mList = new ArrayList<>();    
   @Override
   protected void onCreate(Bundle savedInstanceState) {        
     super.onCreate(savedInstanceState);        setContentView(R.layout.activity_mvp_layout);        initView();        checkIntent();        mListView.setAdapter(new MVPAdapter());    }  
    private void initView() {        mUpgradeBtn = (TextView) findViewById(R.id.mvp_btn);        mUpgradeBtn.setOnClickListener(this);        mListView = (ListView) findViewById(R.id.mvp_listview);    }    
     private void checkIntent() {        Intent intent = getIntent();        
         if (intent != null) {            
         int enter = intent.getIntExtra(EXTRA_ENTER, 0);          
         if (enter == 0) {                errorEnter();            } else {                initData(enter);            }        } else {            errorEnter();        }    }    
   
    /*     * 状态错误     */    private void errorEnter() {        Toast.makeText(this, "Error Intent", Toast.LENGTH_SHORT).show();        finish();    }    
   
    private void initData(int extra) {        
        switch (extra) {            
        case ENTER_A:                mPresenter = new MvpPresenterA(this);              
                 break;            
              case ENTER_B:                mPresenter = new MvpPresenterB(this);                
              break;            
              case ENTER_C:                mPresenter = new MvpPresenterC(this);                
              break;            
              case ENTER_D:                mPresenter = new MvpPresenterD(this);                
              break;            
              default:                errorEnter();                
              break;        }        if (mPresenter != null) {            mPresenter.initData();        }    }    
    @Override    protected void onResume() {        
        super.onResume();        
    if (mPresenter != null) {            mPresenter.onResume();        }    }    
    @Override    public void onClick(View v) {        
            if (v.equals(mUpgradeBtn)) {            Intent intent = new Intent(this, MvpResultActivity.class);            intent.putExtra(MvpResultActivity.EXTRA_ENTER,mPresenter != null ? mPresenter.getEnter() :
            MvpResultActivity.ENTER_MAIN);            
            this.startActivity(intent);        }    }    
   
    @Override    public void initData(List<MvpItem> list) {        mList.clear();        mList.addAll(list);    }
   
    @Override    public void setTitleText(int id) {        setTitle(getString(id));    }    
   
    @Override    public void setUpgradeBtnText(int id) {        mUpgradeBtn.setText(id);    }    
   
    @Override    public void setPresenter(MvpContract.Presenter presenter) {        mPresenter = presenter;    }    
   
    /**     * 列表适配器     *     * @author benhero     */    private class MVPAdapter extends BaseAdapter {        
        @Override        public int getCount() {            
        return mList.size();     }        
       
        @Override        public Object getItem(int position) {            
            return mList.get(position);        }
           
        @Override        public long getItemId(int position) {            
        return position;        }        
       
        @Override        public View getView(int position, View convertView, ViewGroup parent) {            MyViewHolder holder;            
        if (convertView == null) {                holder = new MyViewHolder();                convertView = LayoutInflater.from(MvpActivity.this).inflate(R.layout.mvp_list_item, parent, false);                holder.mIndex = (TextView) convertView.findViewById(R.id.mvp_index);                holder.mTitle = (TextView) convertView.findViewById(R.id.mvp_title);                holder.mDesc = (TextView) convertView.findViewById(R.id.mvp_desc);                holder.mDivider = convertView.findViewById(R.id.mvp_divider);                convertView.setTag(holder);            } else {                holder = (MyViewHolder) convertView.getTag();            }            MvpItem itemBean = mList.get(position);            holder.mIndex.setText(position + 1 + "");            holder.mTitle.setText(itemBean.getTitleId());            holder.mDesc.setText(itemBean.getDescId());            holder.mDivider.setVisibility(position == mList.size() - 1 ? View.GONE : View.VISIBLE);            
       
        return convertView;        }      
       
         /**         * ViewHolder         */        class MyViewHolder {            TextView mIndex;            TextView mTitle;            TextView mDesc;            View mDivider;        }    } }

以上就是我们对V层的处理,根据不同的intent数据,选择不同的MvpPresenter来处理不同的界面数据和交互逻辑。

②. P层

以下是其中某个P层的代码案例。

package com.benhero.design.mvp.presenter;
import com.benhero.design.R;
import com.benhero.design.mvp.bean.MvpItem;
import com.benhero.design.mvp.view.MvpResultActivity;
import java.util.ArrayList;
import java.util.List;

/** * MvpPresenterA * * @author benhero */
public class MvpPresenterA implements MvpContract.Presenter {    
   private final MvpContract.View mView;    
   public MvpPresenterA(MvpContract.View view) {        mView = view;    }    
   
   @Override    public void start() {    }    
   
   @Override    public void initData() {        List<MvpItem> list = new ArrayList<>();        list.add(createFactor(R.string.mvp_a_factor_title_1, R.string.mvp_a_factor_desc_1));        list.add(createFactor(R.string.mvp_a_factor_title_2, R.string.mvp_a_factor_desc_2));        mView.initData(list);        mView.setTitleText(R.string.mvp_a_title);        mView.setUpgradeBtnText(R.string.mvp_a_upgrade_btn);    }    
   
   private MvpItem createFactor(int titleId, int descId) {        MvpItem item = new MvpItem();        item.setTitleId(titleId);        item.setDescId(descId);        
       return item;    }    
   
   @Override    public int getEnter() {        
       return MvpResultActivity.ENTER_A;    }    
   
   @Override    public void onResume() {    } }

③. V层与P层接口

而对于V与P的接口类,是参考谷歌MVP架构开源项目中对于这方面的设计。
具体到本文的案例,接口类如下:

package com.benhero.design.mvp.presenter;

import com.benhero.design.mvp.base.BasePresenter;
import com.benhero.design.mvp.base.BaseView;
import com.benhero.design.mvp.bean.MvpItem;
import java.util.List;/** * MVP接口 * * @author benhero */
public interface MvpContract {    
   /**     * MVP逻辑控制接口     */    interface Presenter extends BasePresenter {        
       void initData();    
       int getEnter();
       void onResume();    }
        
   /**     * MVP界面接口     */    interface View extends BaseView<Presenter> {      
    void initData(List<MvpItem> list);    
    void setTitleText(int id);  
    void setUpgradeBtnText(int id);    } }

4. 弊端

MVP最大的弊端,应该是可读性。
当M层和V层之间的互相调用过多时,在调试或者阅读代码时候,需要不停地在两边不停地跳转。而若不采用MVP,且代码排序良好,则可以自上而下顺畅地阅读。

而影响可读性的另一个重大因素是接口!当你在阅读V层时,遇到一个P的调用,点击跳转,则先跳转到接口类,再点击跳转到实现,实在繁琐(当然也可以通过快捷键直接跳实现的方法。

5. 建议

若M层或者V层不存在复用的可能性,则直接抛弃接口!
接口本身是规范类的行为,从而实现复用,多态。
对于某些业务的开发,根本不存在复用的可能性,可以大胆地抛弃之。
接口还有另一个作用就是约束访问者的访问范围,视情况再决定是否使用。
而对于复用的场景,接口肯定是必不可少的。


二. 模块化

我们开发过程中,经常存在这样的场景:Activity界面可以分成多个模块,且每个模块之间的交互不多。此时,我们就可以采用模块化的思路去解决Activity代码量过大的问题。

1. 思路

其实在实现这方面的需求,Google已经提供了解决方案:Fragment。一个Activity分切成多个Fragment,而且还可以针对不同屏幕来组合视图结构,相当好用。Fragment本身会处理好Activity相关的生命周期,非常棒。

注意:若一个Activity里只包裹着一个Fragment,并且没有别的视图,那么没什么意义!年少时做过不少这种傻事了我。这种场景不如直接一个Activity。

2. 新概念

这里,需要引入一个新的概念:ViewHolder(你也可以用Presenter或者Module等来命名它)。
作用:界面
相关的业务逻辑的封装处理,轻量级。大概基础类如下,可以根据自己的需求进行调整。

package com.benhero.design.module.base;
import android.view.View;
/** * ViewHolder基类 * * @author benhero */
public class ViewHolder {    
       private View mContentView;
       public ViewHolder() {    }  
   
   public ViewHolder(View contentView) {        mContentView = contentView;    }  
    public final void setContentView(View contentView) {        mContentView = contentView;    }  
 
  public View getContentView() {    
        return mContentView;    } }

3. 案例



以下是一个视图模块化比较清晰的界面,图如下,图1抽屉上滑后变成图2的效果,使用的是BottomSheet组件





接下来,将从Activity→Fragment→ViewHolder一层一层展示如何将相对复杂的Activity模块化。

1. Activity

package com.benhero.design.module.activity;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.benhero.design.R;
/** * 模块化Activity * * @author benhero */
public class ModuleActivity extends AppCompatActivity {    
   @Override    protected void onCreate(Bundle savedInstanceState) {        
       super.onCreate(savedInstanceState);        setContentView(R.layout.activity_module);        BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(findViewById(R.id.activity_main_bottom_sheet));        behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);    } }
<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#282828"    tools:context="com.benhero.design.module.activity.ModuleActivity">    <fragment        android:id="@+id/activity_module_bg_fragment"        android:name="com.benhero.design.module.bg.ModuleBgFragment"        android:layout_width="match_parent"        android:layout_height="match_parent"        tools:layout="@layout/fragment_module_bg"/>    <android.support.design.widget.CoordinatorLayout        android:layout_width="match_parent"        android:layout_height="match_parent"        android:clipChildren="false"        android:clipToPadding="false">        <RelativeLayout            android:id="@+id/activity_main_bottom_sheet"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:layout_marginLeft="@dimen/common_margin"            android:layout_marginRight="@dimen/common_margin"            android:clipChildren="false"            android:clipToPadding="false"            app:behavior_hideable="true"            app:behavior_peekHeight="@dimen/main_bottom_sheet_peek_height"            app:elevation="40dp"            app:layout_behavior="android.support.design.widget.BottomSheetBehavior">            <fragment                android:id="@+id/activity_main_bottom_sheet_fragment"                android:name="com.benhero.design.module.bottom.ModuleBottomFragment"                android:layout_width="match_parent"                android:layout_height="match_parent"                tools:layout="@layout/fragment_module_bottom"/>        </RelativeLayout>    </android.support.design.widget.CoordinatorLayout></RelativeLayout>

2. 底层

底层视图相对简单点,就是一个TextView,故没有继续拆分。

package com.benhero.design.module.bg;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.benhero.design.R;
/** * 模块化背景Fragment */
public class ModuleBgFragment extends Fragment {    
   public ModuleBgFragment() {    }  
 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,                             Bundle savedInstanceState) {      
     return inflater.inflate(R.layout.fragment_module_bg, container, false);    } }

3. 抽屉

①. Fragment
package com.benhero.design.module.bottom;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;import android.view.View;
import android.view.ViewGroup;
import com.benhero.design.R;
/** * 模块化抽屉Fragment */
public class ModuleBottomFragment extends Fragment {    
 private ModuleBottomPeekViewHolder mPeekViewHolder;  
   private ModuleBottomListViewHolder mListViewHolder;    
  public ModuleBotto  mFragment() {    }    
 @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,                             Bundle savedInstanceState) {        View layout = inflater.inflate(R.layout.fragment_module_bottom, container, false);        mPeekViewHolder = new ModuleBottomPeekViewHolder(this.getActivity(), layout.findViewById(R.id.bottom_peek_layout));        mListViewHolder = new ModuleBottomListViewHolder(layout.findViewById(R.id.fragment_bottom_sheet_list));        
 return layout;    } }

这里我们通过ViewHolder将抽屉分成了2个模块。另外,布局xml如下。

<LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#6CD1CC"    android:orientation="vertical"    tools:context="com.benhero.design.module.bottom.ModuleBottomFragment">    <LinearLayout        android:id="@+id/bottom_peek_layout"        android:layout_width="match_parent"        android:layout_height="@dimen/main_bottom_sheet_peek_height"        android:orientation="horizontal">        <Button            android:id="@+id/bottom_peek_btn_1"            android:layout_width="0dp"            android:layout_height="match_parent"            android:layout_weight="1"            android:text="Btn 1"/>        <Button            android:id="@+id/bottom_peek_btn_2"            android:layout_width="0dp"            android:layout_height="match_parent"            android:layout_weight="1"            android:text="Btn 2"/>    </LinearLayout>    <include        layout="@layout/fragment_module_bottom_list"/>
</LinearLayout>
②. ViewHolder
package com.benhero.design.module.bottom;

import android.content.Context;
import android.view.View;
import android.widget.Toast;
import com.benhero.design.R;
import com.benhero.design.module.base.ViewHolder;
/** * 抽屉顶部的ViewHolder * * @author benhero */
public class ModuleBottomPeekViewHolder extends ViewHolder implements View.OnClickListener {  
 private final Context mContext;    
 private View mBtn1;    
 private View mBtn2;    
 public ModuleBottomPeekViewHolder(Context context, View contentView) {        super(contentView);        mContext = context;        initView(); }    
 private void initView() {        View contentView = getContentView();        mBtn1 = contentView.findViewById(R.id.bottom_peek_btn_1);        mBtn2 = contentView.findViewById(R.id.bottom_peek_btn_2);        mBtn1.setOnClickListener(this);        mBtn2.setOnClickListener(this);    }
    
  @Override  public void onClick(View view) {        
  if (view.equals(mBtn1)) {            Toast.makeText(mContext, "Click Btn1", Toast.LENGTH_SHORT).show();  } else if (view.equals(mBtn2)) {            Toast.makeText(mContext, "Click Btn2", Toast.LENGTH_SHORT).show();        }    } }

Github

本文案例已上传至Github - DesignExplore  


总结


对于以上两种模式的使用场景,大概如下。

  • 界面视图不可切割模块化,且视图、逻辑都不存在复用的可能:使用MVP,且无需抽接口

  • 界面视图或逻辑存在复用的情况:使用MVP,并抽接口

  • 界面视图可模块化,模块间较少关联:使用视图模块化的方式:Activity→Fragment→ViewHolder

  • 若界面非常复杂,可以考虑两种方式同时使用

对于模块化的方案,不同模块间的通讯可以采用接口让上层去中转。更简单的是使用EventBus。


对于程序设计,每个人的理解可能都不一样,但我们的目标都是一致的,都是想让程序的可读性、逻辑性、拓展性等各方面都达到比较好的效果。

博客链接地址:

奔ben苯笨的博客地址:


http://www.jianshu.com/p/fb057953131e

终端研发部提倡 没有做不到的,只有想不到的。

在这里获得的不仅仅是技术! 


让心,在阳光下学会舞蹈

让灵魂,在痛苦中学会微笑

—终端研发部—



如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :232203809   

微信公众号:终端研发部


            

这里学到的不仅仅是技术






您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存